package ro.dta.idbi.ui.sql;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.event.EventListenerList;

import ro.dta.idbi.interfaces.ITransformationResult;
import ro.dta.idbi.interfaces.IntegrationOperation;
import ro.dta.idbi.interfaces.sql.ISqlColumn;
import ro.dta.idbi.interfaces.sql.ISqlConstruct;
import ro.dta.idbi.interfaces.sql.ISqlTable;
import ro.dta.idbi.interfaces.ui.AbstractDiagram;
import ro.dta.idbi.interfaces.ui.IExecIqlDialog;
import ro.dta.idbi.interfaces.ui.IMainFrame;
import ro.dta.idbi.model.AbstractPattern;
import ro.dta.idbi.model.Pattern;
import ro.dta.idbi.model.Utils;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.reps.Construct;
import uk.ac.ic.doc.automed.reps.Schema;
import uk.ac.ic.doc.automed.reps.SchemaObject;
import uk.ac.ic.doc.automed.reps.Transformation;

import com.mxgraph.io.mxCellCodec;
import com.mxgraph.io.mxCodec;
import com.mxgraph.io.mxCodecRegistry;
import com.mxgraph.layout.hierarchical.mxHierarchicalLayout;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxGraph;

/**
 * Sql Schema class Used to display a schema in the AutoMed repository as a
 * graph containing tables, columns, primary keys and foreign keys
 * 
 * @author Tudor Dobrila
 * 
 */
public class SqlSchemaDiagram extends AbstractDiagram {

	private static final long serialVersionUID = 4039197311737928111L;

	private static final Logger LOGGER = Logger.getLogger("com.dta.idbi.sql.ui.SqlSchemaDiagram");

	/**
	 * Graph model
	 */
	protected transient mxGraph graph;

	/**
	 * AutoMed schema associated with this diagram
	 */
	protected transient Schema repositorySchema;

	/**
	 * Event listener list
	 */
	private final transient EventListenerList listenerList = new EventListenerList();

	/**
	 * Integration operation used to display the appropriate transformation
	 * patterns when right click-ing a construct. Can be CONFORMING, MERGING or
	 * IMPROVING
	 */
	protected transient IntegrationOperation operation;

	/**
	 * True if schema has position stored for at least on of the tables in the
	 * AutoMed repository, false otherwise<br />
	 * If no positions are stored, perform an auto layout
	 */
	private boolean hasPosition;

	/**
	 * SqlSchema panel class constructor
	 * 
	 * @param repositorySchema
	 *            Schema that will be represented in the panel
	 * @param parentFrame
	 *            Main frame of the application
	 * @throws Exception
	 */
	public SqlSchemaDiagram(final IMainFrame parentFrame, final Schema repositorySchema) {
		this(parentFrame, repositorySchema, null);
	}

	/**
	 * SqlSchema panel class constructor
	 * 
	 * @param repositorySchema
	 *            Schema that will be represented in the panel
	 * @param parentFrame
	 *            Main frame of the application
	 * @param operation
	 *            Integration operation used to display the appropriate
	 *            transformation patterns when right click-ing a construct. Can
	 *            be CONFORMING, MERGING or IMPROVING
	 * @throws Exception
	 */
	public SqlSchemaDiagram(final IMainFrame parentFrame, final Schema repositorySchema,
			final IntegrationOperation operation) {
		super(parentFrame);
		this.repositorySchema = repositorySchema;
		this.operation = operation;

		setLayout(new BorderLayout());
		setBackground(Color.WHITE);

		// Get model name and verify if it's valid
		@SuppressWarnings("rawtypes")
		final Map models = repositorySchema.getModels();
		final Object[] keys = models.keySet().toArray();
		for (Object o : keys) {
			final String modelName = o.toString();
			if (!modelName.startsWith("sql_") && !modelName.startsWith("source_sql_")) {
				throw new IllegalArgumentException(
						"Schema is not in the SQL / Source_SQL data models.");
			}
		}

		// Initialise graph structure
		graph = new SqlGraph();
		populateGraph();

		graphComponent = new mxGraphComponent(graph);
		graphComponent.setToolTips(true);
		graphComponent.setConnectable(false);
		graphComponent.setImportEnabled(false);
		graphComponent.getGraphControl().addMouseListener(new GraphMouseAdapter());
		graphComponent.getGraphControl().setName("graph");
		graphComponent.getViewport().setBackground(Color.WHITE);
		graph.addListener(mxEvent.CELLS_MOVED, new mxIEventListener() {

			@Override
			public void invoke(Object arg0, mxEventObject arg1) {
				Object[] selTables = (Object[]) arg1.getProperty("cells");
				for (Object tbl : selTables) {
					// Update the location of the table in the AutoMed
					// repository
					ISqlTable tblCell = (ISqlTable) tbl;
					saveTablePosition(tblCell);
				}
			}
		});
		add(graphComponent, BorderLayout.CENTER);
	}

	/**
	 * Populate the graph component with the information in the SQL schema
	 */
	private void populateGraph() {
		hasPosition = false;
		final Object parent = graph.getDefaultParent();
		graph.getModel().beginUpdate();

		// Go through the elements of the schema and display them
		try {
			final Map<String, ISqlTable> tables = new HashMap<String, ISqlTable>();
			final SchemaObject[] components = repositorySchema.getSchemaObjects();
			int xPos = 20;

			// Display schema components
			for (SchemaObject o : components) {
				Construct oConstr = o.getConstruct();
				if (oConstr.equals(AbstractPattern.table) || oConstr.equals(AbstractPattern.srcTable)) {
					// If construct is of type table, check if a position for it
					// has been stored in the AutoMed repository, otherwise
					// assign default position
					Point2D pos = getTablePosition(o, xPos);
					final ISqlTable table = new SqlTableCell(repositorySchema, o, null,
							o.getValue(), pos.getX(), pos.getY(), 150, 30, "TABLE");
					graph.addCell(table, parent);
					tables.put(o.getValue(), table);
					xPos += 200;

					// Get columns
					SchemaObject[] columns = repositorySchema.getDependentSchemaObjects(o,
							AbstractPattern.column);
					for (SchemaObject col : columns) {
						// If construct is of type column, get parent table
						final Point currentPos = table.getPosition();
						final Object[] schemeDef = col.getSchemeDefinition();
						final boolean isNullable = schemeDef[2].toString().equals("null");
						final String isNull = isNullable ? "?" : "";
						final SqlColumnCell currentCell = new SqlColumnCell(repositorySchema, col,
								null, col.getValue() + isNull, 0, currentPos.getY(), 150, 23,
								"COLUMN");

						table.addColumn(currentCell);
						currentCell.setNullable(isNullable);
						currentCell.setType(schemeDef[3].toString());
						graph.addCell(currentCell, table);
						currentPos.translate(0, 25);
					}

					// Get PK constraints
					SchemaObject[] pks = repositorySchema.getDependentSchemaObjects(o,
							AbstractPattern.primaryKey);
					for (SchemaObject pk : pks) {
						Object[] pkDef = pk.getSchemeDefinition();
						for (int i = 2; i < pkDef.length; i++) {
							SchemaObject pkCol = (SchemaObject) pkDef[i];
							ISqlColumn crtCol = table.getColumn(pkCol.getValue());
							crtCol.setPK(true);
							crtCol.setStyle("PRIMARYKEY");
						}
					}
				}
			}

			Collection<ISqlTable> tbls = tables.values();
			// Compute the supertype tables
			for (ISqlTable o1 : tbls) {
				List<String> supertypes = o1.getSupertypesNames();
				for (String s : supertypes) {
					o1.addSupertype(tables.get(s));
					tables.get(s).addChild(o1);
				}
			}

			// Get FK edges
			for (ISqlTable o : tbls) {
				// Get FK constraints
				SchemaObject[] fks = repositorySchema.getDependentSchemaObjects(
						o.getSchemaObject(), AbstractPattern.foreignKey);
				for (SchemaObject fk : fks) {
					Object[] fkDef = fk.getSchemeDefinition();
					SchemaObject source = (SchemaObject) fkDef[1];
					if (source.getValue().equals(o.getValue())) {
						final int half = (fkDef.length - 3) / 2;
						final ISqlTable sourceCell = tables.get(o.getValue());
						final ISqlTable targetCell = tables.get(((SchemaObject) fkDef[2 + half])
								.getValue());
						final SqlForeignKeyCell fkCell = new SqlForeignKeyCell(repositorySchema,
								fk, "", fkDef[0].toString(), sourceCell, targetCell, "FK_EDGE");
						graph.addCell(fkCell, parent);

						// add source and target cells
						for (int i = 0; i < half; i++) {
							SchemaObject sourceCol = (SchemaObject) fkDef[2 + i];
							fkCell.addSourceCol(sourceCell.getColumn(sourceCol.getValue()));
							SchemaObject targetCol = (SchemaObject) fkDef[3 + half + i];
							fkCell.addTargetCol(targetCell.getColumn(targetCol.getValue()));
						}

						sourceCell.addForeignKey(fkCell);
					}
				}
			}
		} finally {
			if (!hasPosition) {
				performLayout();
			}
			graph.setAutoSizeCells(true);
			graph.getModel().endUpdate();
		}
	}

	/**
	 * Perform an automatic layout of the diagram
	 */
	public void performLayout() {
		final mxHierarchicalLayout layout = new mxHierarchicalLayout(graph, SwingConstants.WEST);
		layout.execute(graph.getDefaultParent());

		// Save the position of the tables to the AutoMed repository
		Object[] vertices = mxGraphModel.getChildVertices(graph.getModel(),
				graph.getDefaultParent());
		for (Object v : vertices) {
			if (v instanceof ISqlTable) {
				saveTablePosition((ISqlTable) v);
			}
		}
	}

	/**
	 * Get the stored position of a table from the AutoMed repository
	 * 
	 * @param object
	 *            Schema object representing the table
	 * @param xPos
	 *            Default X position, if no stored position for this table can
	 *            be found in the repository
	 * @return Position of the table on the diagram
	 */
	private Point2D getTablePosition(SchemaObject object, int xPos) {
		double tblX, tblY;

		try {
			String pos = object.getSchema().getProperty("pos_" + object.getOBID());
			String[] posToken = pos.split(",");
			tblX = Double.parseDouble(posToken[0]);
			tblY = Double.parseDouble(posToken[1]);
			hasPosition = true;
		} catch (NotFoundException e) {
			// No position exists for the current table, assign
			// default position
			Transformation[] trans = object.getTransformations();
			for (Transformation t : trans) {
				if ("rename".equals(t.getActionName())) {
					SchemaObject[] refObj = t.getReferencedSchemaObjects();
					SchemaObject targetTbl = refObj[1].getReferencedSchemaObjects()[0];
					if (targetTbl.equals(object)) {
						SchemaObject sourceTbl = refObj[0].getReferencedSchemaObjects()[0];
						return getTablePosition(sourceTbl, xPos);
					}
				}
			}

			tblX = xPos;
			tblY = 20;
		}

		return new Point2D.Double(tblX, tblY);
	}

	/**
	 * Save the position of a table in the AutoMed repository
	 * 
	 * @param table
	 *            Table for which to save the position
	 */
	private void saveTablePosition(ISqlTable table) {
		mxGeometry geom = table.getGeometry();
		String posStr = geom.getX() + "," + geom.getY();
		table.getSchemaObject().getSchema()
				.setProperty("pos_" + table.getSchemaObject().getOBID(), posStr);
	}

	/**
	 * 
	 * @return The graph component used to display the SQL diagram
	 */
	public mxGraphComponent getGraphComponent() {
		return graphComponent;
	}

	/**
	 * Get the popup menu for a given selected item
	 * 
	 * @return Popup menu for the selected item
	 */
	private JPopupMenu getPopupMenu() {
		final JPopupMenu cellPopup = new JPopupMenu();
		Object[] selObj = graph.getSelectionCells();

		if (selObj.length == 1
				&& (selObj[0] instanceof ISqlTable || selObj[0] instanceof ISqlColumn)) {
			// Add the query object item, used to query the object and display
			// the results in a dialog box
			JMenu toolsMenu = new JMenu("Tools");
			cellPopup.add(toolsMenu);

			JMenuItem queryItem = new JMenuItem("Query object");
			queryItem.setIcon(new ImageIcon("./resources/query.png"));
			queryItem.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent paramActionEvent) {
					showIqlDialog(graph.getSelectionCells()[0]);
				}
			});
			toolsMenu.add(queryItem);

			if (selObj[0] instanceof ISqlTable) {
				final ISqlTable crtTbl = (ISqlTable) selObj[0];
				JMenuItem fdItem = new JMenuItem("Functional dependency discovery");
				fdItem.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent paramActionEvent) {
						FDTool fdDlg = new FDTool(parentFrame, repositorySchema.getName(), crtTbl
								.getValue().toString());
						fdDlg.setVisible(true);
					}
				});
				toolsMenu.add(fdItem);
			}
		}

		if (operation != null) {
			try {
				final List<Pattern> patterns = Utils.getPatterns(operation);
				for (final Pattern p : patterns) {
					if (p.hasUI() && p.isValid(repositorySchema, graph.getSelectionCells())) {
						final JMenuItem item = new JMenuItem(p.getName());
						item.setToolTipText(p.getDescription());
						item.addActionListener(new ActionListener() {

							@Override
							public void actionPerformed(final ActionEvent event) {
								try {
									final Object[] selObj = graph.getSelectionCells();
									final ISqlConstruct[] sel = new ISqlConstruct[selObj.length];
									for (int i = 0; i < selObj.length; i++) {
										sel[i] = (ISqlConstruct) selObj[i];
									}

									final JLabel beforeLbl = parentFrame.getGuiFactory()
											.createTitleLabel("Before transformation:");
									final JLabel afterLbl = parentFrame.getGuiFactory()
											.createTitleLabel("After transformation:");
									final ITransformationResult res = p.showUI(parentFrame,
											repositorySchema, sel, beforeLbl, afterLbl);
									if (res != null) {
										fireSchemaChangedEvent(new SchemaChangedEvent(
												SqlSchemaDiagram.this, res));
									}
								} catch (Exception e1) {
									LOGGER.log(Level.SEVERE, "Fatal error", e1);
								}

							}
						});
						cellPopup.add(item);
					}
				}
			} catch (Exception e1) {
				JOptionPane.showMessageDialog(SqlSchemaDiagram.this,
						"Could not load pattern.\r\nReason: " + e1.getMessage());
			}
		}

		return cellPopup;
	}

	/**
	 * Add a schema changed listener
	 * 
	 * @param listener
	 *            Schema changed listener
	 */
	public void addSchemaChangedEventListener(final SchemaChangedListener listener) {
		listenerList.add(SchemaChangedListener.class, listener);
	}

	/**
	 * Remove a schema changed listener
	 * 
	 * @param listener
	 *            Schema changed listener
	 */
	public void removeSchemaChangedListener(final SchemaChangedListener listener) {
		listenerList.remove(SchemaChangedListener.class, listener);
	}

	/**
	 * Fire schema changed event, when the schema displayed in the diagram is
	 * changed
	 * 
	 * @param evt
	 *            Schema changed event
	 */
	private void fireSchemaChangedEvent(final SchemaChangedEvent evt) {
		final Object[] listeners = listenerList.getListenerList();

		for (int i = 0; i < listeners.length; i += 2) {
			if (listeners[i] == SchemaChangedListener.class) {
				((SchemaChangedListener) listeners[i + 1]).schemaChanged(evt);
			}
		}
	}

	/**
	 * Set the new schema being displayed in the diagram
	 * 
	 * @param schema
	 *            New schema being displayed in the diagram
	 * @param result
	 *            Transformation result
	 */
	public void setSchema(final Schema schema, ITransformationResult result) {
		this.repositorySchema = schema;
		fireSchemaChangedEvent(new SchemaChangedEvent(this, result));
	}

	/**
	 * Set the schema currently being displayed in the diagram
	 * 
	 * @param schema
	 *            Schema being displayed in the diagram
	 * @param operation
	 *            Currently performed integration operation
	 */
	public void setSchema(final Schema schema, final IntegrationOperation operation) {
		this.repositorySchema = schema;
		this.operation = operation;

		graph.setModel(new mxGraphModel());
		populateGraph();
	}

	/**
	 * Show the IQL window used to display the contents of a column or a table
	 * 
	 * @param selectedObj
	 *            Selected object, must be a column or a table
	 */
	private void showIqlDialog(Object selectedObj) {
		if (!(selectedObj instanceof ISqlTable) && !(selectedObj instanceof ISqlColumn)) {
			return;
		}

		// Double click -> show data to the user
		String query = "";
		if (selectedObj instanceof ISqlTable) {
			query = ((ISqlTable) selectedObj).getIqlQuery();
		} else {
			query = ((ISqlColumn) selectedObj).getShortDefinition();
		}
		IExecIqlDialog queryDlg = parentFrame.getGuiFactory().createExecIqlDialog(parentFrame, repositorySchema, query);
		queryDlg.setVisible(true);
	}

	/**
	 * SQL schema graph mouse adapter
	 * 
	 * @author Tudor Dobrila
	 * 
	 */
	class GraphMouseAdapter extends MouseAdapter {
		@Override
		public void mouseReleased(final MouseEvent event) {
			final JComponent source = (JComponent) event.getSource();
			Object[] selCells = graph.getSelectionCells();

			if (event.getButton() == MouseEvent.BUTTON3 && source.getName().equals("graph")) {
				final JPopupMenu cellPopup = getPopupMenu();
				cellPopup.show(event.getComponent(), event.getX(), event.getY());
			}

			if (selCells.length == 1
					&& (selCells[0] instanceof ISqlTable || selCells[0] instanceof ISqlColumn)
					&& event.getClickCount() == 2 && event.getButton() == MouseEvent.BUTTON1) {
				showIqlDialog(selCells[0]);
			}
		}
	}

	@Override
	public mxCodec getCodec() {
		final mxCodec codec = new mxCodec();
		mxCodecRegistry.register(new mxCellCodec(new SqlTableCell(null, null, "", "", null, ""),
				new String[] { "columns", "position" }, null, null));
		mxCodecRegistry.register(new mxCellCodec(new SqlColumnCell(null, null, "", "", null, ""),
				new String[] { "isPK", "isNullable" }, null, null));
		mxCodecRegistry.register(new mxCellCodec(new SqlForeignKeyCell(null, null, "", "", null, null,
				""), null, null, null));
		return codec;
	}
}
